package cn.bonoon.handler.impl;

import java.io.IOException;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.util.Assert;

import cn.bonoon.kernel.exceptions.MethodErrorException;
import cn.bonoon.kernel.security.LogonUser;
import cn.bonoon.kernel.util.MD5Util;
import cn.bonoon.util.VerifyCodeHelper;

/**
 * <pre>
 * 登录时，对使用验证码的情况进行支持
 * 
 * 可以设置开启或不开启使用验证码的功能
 * 
 * </pre>
 * 使用默认的
 * {@link org.springframework.security.authentication.AuthenticationProvider AuthenticationProvider}
 * 为：
 * {@link org.springframework.security.authentication.dao.DaoAuthenticationProvider DaoAuthenticationProvider}
 * <p>
 * 记住我的功能需要实现
 * {@link org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices AbstractRememberMeServices}
 * 类来实现自动登录的功能
 * </p>
 * <pre>
 * 参照以下spring的类{@link org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter UsernamePasswordAuthenticationFilter}
 * </pre>
 * @author jackson
 * @see AbstractAuthenticationProcessingFilter
 */
public class AuthenticationExtendFilter extends AbstractAuthenticationProcessingFilter{
    public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "j_username";
    public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "j_password";
    // 是否开启验证码功能  
    private static final int validateCodeType = 1;  
	// 验证码字段  
    private String validateCodeParameter = "validateCode";  
    private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
    private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
    
    private boolean useVerifyCode = true;
    
    /** 尝试登录次数的限制 */
    private int tryLoginLimit;
    
    /** 尝试登录的间隔时间，毫秒 */
    private long tryLoginInterval;

	String findUsername(AuthenticationException failed){
		@SuppressWarnings("deprecation")
		Authentication auth = failed.getAuthentication();
		String uname = null;
		if(null != auth){
			Object pri = auth.getPrincipal();
			if(pri instanceof LogonUser){
				uname = ((LogonUser) pri).getUsername();
			}else if(pri instanceof String){
				uname = (String)pri;
			}
		}
		return uname;
	}
	
	private enum LoginingStatus{
		/** 拒绝，不允许登录 */
		DENIED{
			@Override
			public boolean denied() {
				return true;
			}
		},
		/** 虽然已经拒绝访问，但等待时间已经超过了指定的时间，所以可以访问 */
		TIMEOUT,
		/** 尝试登录没有超过规定的次数，可以访问 */
		ACCESS,
		/** 当前正在尝试登录的用户属于非系统账号 */
		NOUSER;
		
		public boolean denied(){ return false; }
	}
	
    /**
     * 同个session的用户登录行为的控制
     * 
     * @author jackson
     *
     */
    private class LoginControl{
    	
    	private long start;
    	
    	/** 尝试登录的次数 */
    	private int tryLoginCount;
    	
    	private LoginControl next;
    	
    	private final String username;
    	
    	LoginControl(AuthenticationException failed){
    		this(findUsername(failed));
    	}
    	
    	LoginControl(String username){
    		this.username = username;
    		loginUnsuccess();
    	}
    	
    	LoginingStatus denied(){
    		if(0 < tryLoginLimit && tryLoginLimit <= tryLoginCount){
    			//这里是拒绝访问
	    		if(tryLoginInterval > 1000){
	    			long current = System.currentTimeMillis() - start;
	    			if(tryLoginInterval < current){
	    				/*
	    				 * 时间已经超过规定的时间，可以访问
	    				 * 所以在这个时候，应该重新计数
	    				 */
	    				tryLoginCount = 0;
	    				return LoginingStatus.TIMEOUT;
	    			}
	    		}
    			return LoginingStatus.DENIED;
    		}
    		return LoginingStatus.ACCESS;
    	}
    	
    	/** 拒绝登录 */
    	LoginingStatus denied(String username){
    		
	    	if(username.equals(this.username)){
	    		return denied();
	    	}
	    	
	    	if(null != next) {
	    		LoginingStatus nextDenied = next.denied(username);
	    		if(nextDenied == LoginingStatus.NOUSER && null == this.username){
	    			return denied();
	    		}
	    		return nextDenied;
	    	}
	    	
	    	return LoginingStatus.NOUSER;
    	}
    	
    	boolean checkedUser(String uname){
    		return (null == uname && null == username) || (null != uname && uname.equals(username));
    	}
    	
    	void loginUnsuccess(AuthenticationException failed){
    		String uname = findUsername(failed);
    		loginUnsuccess(uname);
    	}
    	
    	void loginUnsuccess(String uname){
    		if(checkedUser(uname)){
    			loginUnsuccess();
    			
    			return;
    		}
    		
    		if(null == next){
    			next = new LoginControl(uname);
    		}else{
    			next.loginUnsuccess(uname);
    		}
    	}
    	
    	void loginUnsuccess(){
    		this.start = System.currentTimeMillis();
    		this.tryLoginCount++;
    	}
    	
    }
    
    private final static String LOGIN_CONTROL_NAME = MD5Util.randomMD5String();
    
    public AuthenticationExtendFilter() {
        super("/j_spring_security_check");
    }
    
    @Override  
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {  
        // 只接受POST方式传递的数据  
        if (!"POST".equals(request.getMethod())){  
            throw new MethodErrorException("不支持非POST方式的请求!");  
        }
        String username = obtainUsername(request);  
        
        LoginControl lc = (LoginControl)request.getSession().getAttribute(LOGIN_CONTROL_NAME);
        if(null != lc && lc.denied(username).denied()){
        	throw new MethodErrorException("您尝试登录的数据过频繁，请稍后再登录！");
        }
        
        if(useVerifyCode){
	        // 开启验证码功能的情况  
	        VerifyCodeHelper.verify(request, validateCodeType, obtainValidateCodeParameter(request));
        }
        
        // 获取Username和Password  
        String password = obtainPassword(request);  
  
        // UsernamePasswordAuthenticationToken实现Authentication校验  
        AbstractAuthenticationToken authRequest = __authentication(request, username, password);  
  
        // 允许子类设置详细属性  
        authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
        
        // 运行UserDetailsService的loadUserByUsername 再次封装Authentication  
        Authentication auth = this.getAuthenticationManager().authenticate(authRequest);
        //TODO 这里是某个用户在某个子系统登录已经验证成功了，但他不被允许进入这个子系统的情况
        
        return auth;
    }
    
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request,
    		HttpServletResponse response, AuthenticationException failed)
    		throws IOException, ServletException {
    	LoginControl lc = (LoginControl)request.getSession().getAttribute(LOGIN_CONTROL_NAME);
    	if(lc == null){
    		request.getSession().setAttribute(LOGIN_CONTROL_NAME, lc = new LoginControl(failed));
    	}else{
    		lc.loginUnsuccess(failed);
    	}
    	super.unsuccessfulAuthentication(request, response, failed);
    }
    
    @Override
    protected void successfulAuthentication(HttpServletRequest request,
    		HttpServletResponse response, FilterChain chain,
    		Authentication authResult) throws IOException, ServletException {
    	request.getSession().removeAttribute(LOGIN_CONTROL_NAME);
    	super.successfulAuthentication(request, response, chain, authResult);
    }
    
    protected AbstractAuthenticationToken __authentication(HttpServletRequest request, String username, String password){
        // UsernamePasswordAuthenticationToken实现Authentication校验  
        return new UsernamePasswordAuthenticationToken(username, password);  
    }
    
    private String __checkString(String str){
    	return null == str ? "" : str.trim();
    }
  
    public String obtainValidateCodeParameter(HttpServletRequest request) {  
    	return __checkString(request.getParameter(validateCodeParameter));  
    }  
  
    protected String obtainUsername(HttpServletRequest request) {  
    	return __checkString(request.getParameter(usernameParameter)); 
    }  
  
    protected String obtainPassword(HttpServletRequest request) {  
    	return __checkString(request.getParameter(passwordParameter)); 
    }  
  
    public void setValidateCodeParameter(String validateCodeParameter) {  
        this.validateCodeParameter = validateCodeParameter;  
    }

	public void setUseVerifyCode(boolean useVerifyCode) {
		this.useVerifyCode = useVerifyCode;
	}  

    /**
     * Sets the parameter name which will be used to obtain the username from the login request.
     *
     * @param usernameParameter the parameter name. Defaults to "j_username".
     */
    public void setUsernameParameter(String usernameParameter) {
        Assert.hasText(usernameParameter, "Username parameter must not be empty or null");
        this.usernameParameter = usernameParameter;
    }

    /**
     * Sets the parameter name which will be used to obtain the password from the login request..
     *
     * @param passwordParameter the parameter name. Defaults to "j_password".
     */
    public void setPasswordParameter(String passwordParameter) {
        Assert.hasText(passwordParameter, "Password parameter must not be empty or null");
        this.passwordParameter = passwordParameter;
    }

    public final String getUsernameParameter() {
        return usernameParameter;
    }

    public final String getPasswordParameter() {
        return passwordParameter;
    }
    
    public void setTryLoginLimit(int tryLoginLimit) {
		this.tryLoginLimit = tryLoginLimit;
	}
    
    public void setTryLoginInterval(long tryLoginInterval) {
		this.tryLoginInterval = tryLoginInterval;
	}
}
